// --------------------------------------------------------------------------------------------------------------------
// <copyright file="workProvider.cs" company="AzureContrib (Open source library http://azurecontrib.codeplex.com)">
//   Microsoft Public License (Ms-PL)
// </copyright>
// <summary>
//   A Work Service that finds, schedules and hands out <see cref="IWorkItem" />. <seealso cref="IWorkProvider" />
// </summary>
// --------------------------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.ComponentModel.Composition;
using System.Linq;
using AzureContrib.WindowsAzure.Diagnostics;
using AzureContrib.WindowsAzure.ServiceRuntime;
using AzureContrib.WindowsAzure.ServiceRuntime.ServiceLocation;

namespace AzureContrib.WindowsAzure.WorkServices
{
	/// <summary>
	/// A Work Service that finds, schedules and hands out <see cref="IWorkItem"/>. <seealso cref="IWorkProvider"/>
	/// </summary>
	[Export(typeof(IWorkProvider))]
	public class WorkProvider : IWorkProvider
	{
		private IEnumerable<WorkTemplate> works;
		private bool throwOnWorkFail;

		private IBuilder builder;

		/// <summary>
		/// Initializes a new instance of the <see cref="WorkProvider"/> class. 
		/// </summary>
		/// <param name="traceFactory">
		/// Creates the <see cref="ITrace"/> we trace to.
		/// </param>
		/// <param name="roleEnvironment">
		/// The current <see cref="IRoleEnvironment"/>
		/// </param>
		[ImportingConstructor]
		public WorkProvider(ITraceFactory traceFactory, IRoleEnvironment roleEnvironment)
		{
			Assumes.NotNull(traceFactory, "traceFactory");
			Assumes.NotNull(roleEnvironment, "roleEnvironment");
			Trace = traceFactory.Create(GetType());

			bool settingValue;

			// If there is a setting; use it. Otherwise TRUE!
			throwOnWorkFail = !roleEnvironment.TryGetBoolConfigurationSetting(WorkServiceNames.ThrowOnWorkExceptionConfigurationName, Trace, out settingValue) ||
												settingValue;
		}

		/// <summary>
		/// Gets or sets a <see cref="IWorkSchedulerProvider"/> to get the current work scheduler from.
		/// </summary>
		[Import]
		public IWorkSchedulerProvider WorkSchedulerProvider { get; set; }

		/// <summary>
		/// Gets or sets the current <see cref="ITrace"/>
		/// </summary>
		public ITrace Trace { get; set; }

		/// <summary>
		/// Gets or sets the current <see cref="IWorkItem"/> tasks in the application.
		/// </summary>
		[Import]
		public IEnumerable<Lazy<IWork>> Works
		{
			get
			{
				return works;
			}

			set
			{
				works = value.Select(lw =>
					{
						var workTemplate = new WorkTemplate(lw, Trace);

						// It can be null!
						workTemplate.SetBuilder(builder);
						return workTemplate;
					}).ToArray();
				Trace.Information("Located the following IWork items: {0}", WorksAsString(works));
			}
		}

		/// <summary>
		/// Get a piece of work to perfom <see cref="IWorkProvider.GetWork"/>.
		/// </summary>
		/// <returns>The work to perform.</returns>
		public IWork GetWork()
		{
			Lazy<IWork> workToDo = GetAllLazyWork(WorkType.Run).FirstOrDefault();
			return workToDo != null ? workToDo.Value : new IdleWork(Trace);
		}

		/// <summary>
		/// Get all work of a certain <paramref name="workType"/>.
		/// </summary>
		/// <param name="workType">The work Type.</param>
		/// <returns>All work.</returns>
		public IEnumerable<IWork> GetAllWork(WorkType workType)
		{
			return GetAllWork(workType, false);
		}

		/// <summary>
		/// Get all work of a certain <paramref name="workType"/>.
		/// </summary>
		/// <param name="workType">The work Type.</param>
		/// <param name="includeWorksNotPending">Filter to call or not the <see cref="IWorkItem.HasWork"/>.</param>
		/// <returns>All work.</returns>
		public IEnumerable<IWork> GetAllWork(WorkType workType, bool includeWorksNotPending)
		{
			return GetAllLazyWork(workType, includeWorksNotPending).Select(w => w.Value);
		}

		/// <summary>
		/// Sets the global <see cref="IBuilder"/> for access to <see cref="IBuilder.BuildUp"/> for instances not created through Inversion of Control.
		/// </summary>
		/// <param name="builder">The current global <see cref="IBuilder"/>.</param>
		public void SetBuilder(IBuilder builder)
		{
			// Here we set the IBuilder access to the underlying structure.
			// No instances of the actual IWorkItems have been created yet and won't be until IWorkItem.HasWork or IWorkItem.DoWork() is called.
			// At that time the Builder will be accessed for Property Injection.
			// (Phew!)
			this.builder = builder;
			works.ForEach(lbu => lbu.SetBuilder(this.builder));
		}

		/// <summary>
		/// Get all work but keep them <see cref="Lazy{T}"/>.
		/// </summary>
		/// <param name="workType">Only work of this <see cref="WorkType"/>.</param>
		/// <param name="includeWorksNotPending">Filter to call or not the <see cref="IWorkItem.HasWork"/>.</param>
		/// <returns>All lazy work.</returns>
		internal IEnumerable<Lazy<IWork>> GetAllLazyWork(WorkType workType, bool includeWorksNotPending = false)
		{
			var workOfWorkType = Works.Where(wi => wi.Value.WorkType == workType);

			var workScheduler = WorkSchedulerProvider.CurrentWorkScheduler();

			var orderedWorksOfWorkType = workScheduler.ScheduleWork(workOfWorkType);

			return orderedWorksOfWorkType.Where(wi => EvaluateHasWork(wi, includeWorksNotPending));
		}

		private string WorksAsString(IEnumerable<Lazy<IWork>> lazyWorks)
		{
			var lazyWorksAsStrings = lazyWorks.OrderBy(lw => lw.Value.WorkType)
				.ThenBy(lw => lw.Value.Order)
				.Select(lw => "({0} {1}) {2}".F(lw.Value.WorkType, lw.Value.Order, lw.Value.Name));
			string worksAsString = string.Join(", ", lazyWorksAsStrings);
			return string.IsNullOrEmpty(worksAsString) ? "none" : worksAsString;
		}

		private bool EvaluateHasWork(Lazy<IWork> wi, bool includeWorksNotPending)
		{
			try
			{
				return includeWorksNotPending || wi.Value.HasWork;
			}
			catch (Exception e)
			{
				var message = "Evaluating .HasWork for IWork {0} threw exception {1} (Message: {2}, Stack trace: {3}).".F(wi.Value.Name, e.GetType().FullName, e.Message, e.StackTrace);
				Trace.Error(message);

				if (throwOnWorkFail)
				{
					throw new WorkException(message, e);
				}

				return false;
			}
		}
	}
}